1.  Language Implementation Details

The ode input language is implemented with a bison-generated parser and a
flex-generated lexical analyzer.  The benefits of this approach are great.
The original version of the basic recognizer was completed in about two
days.  And adding comments to the language required that exactly one line
of the lexical analyzer source code be changed.

Differential equations are compiled into linked lists of stack operations,
one list per dynamic variable.  The function evaluator (eval) traverses one
of these lists, applying the indicated operations to a small operand stack.
At the completion of the traverse, the new derivative is on top of the
stack.  The routine pops and returns this result.  Another routine (field)
is called by the numerical routines whenever a re-evaluation of the
gradient is required.  It calls eval() for each dynamic variable in turn.

Changes to the language involve modifications to the bison grammar, the
flex rules, and/or the semantics stored with the grammar and rules.  The
steps required to add a new builtin function, for example, are (aside from
writing the function itself) as follows.

(1)  Invent a new terminal symbol name for the reserved word.

(2)  Add the new reserved word to the flex rules.

(3)  Add the new productions to the right-hand side of
     "expr" in the grammar, together with whatever semantic
     action is required (using the included macros if possible).

(4)  Define a new value for exper and add the stack code to eval.

2.  Numerical Schemes

The numerical and language procedures were designed in accordance with the
Jesuit maxim.  The major elements manipulated by both are the symbol table,
and the functions field() and printq().  Addition of a new numerical scheme
requires that the numerical routine be written, and perhaps that some
fields inside the symbol table entry be expanded or added.  None of these
changes affects the performance or compromise the integrity of the parser,
although they may change the space-cost of a dynamic variable.  Similarly,
modifications to the lexical analyzer or the grammar only affect the parse
tables and related data structures, and have no effect on the numerical
routines.

At each step the numerical scheme fills each symbol table entry with
relevant values.  Then a call to field() evaluates every differential
equation and puts the result in the symbol table element syrime.  The
evaluator gets current values for the dynamics from the field syalue.  Both
syalue and syrime are in some sense temporary variables.  All correct
results eventually find their way into syal[0] and syri[0]; these are never
touched by the language processor.  Thus the real interface between the
language and numerical procedures is merely syalue and syrime.  It is
important to maintain meaningful values in syalue and syrime, since a
floating-point exception may disrupt the control flow at any moment.  It is
recommended that the pointer fsp be used in traversing the symbol table, so
that arithmetic exceptions can be diagnosed as accurately as possible.

Fourth and fifth order schemes were chosen as the best compromise between
speed and accuracy.  The adaptive stepsize has proved a boon in both areas.

3.  Details of Error and Fault Recovery

Command-line errors are diagnosed at initialization time and are generally
fatal.

Errors in the input stream are diagnosed during the lexical scan or at
parse time.  They elicit an appropriate message.  The error recovery method
used in the parser is the vanilla Yacc scheme.  (see S. Johnson, "Yacc: Yet
Another Compiler-Compiler", 1978, pp. 16-18).

Run-time errors are detected by the function evaluator, the numerical
routines, or the floating-point unit hardware.  In each case a message is
printed describing the error, and ode returns to a known state awaiting
input.  The current step is abandoned, but all variable values are left
reflecting the state of the machine just prior to the fault.  All of the
numerical schemes scan the symbol table, computing values for each dynamic
variable, using a common pointer known to the error recovery routines named
fsp.  This pointer provides a way of identifying the offending dynamic
variable when a trap occurs.

4. Space Utilization

The data space used in numerically solving a system of ODEs consists of
both fixed and dynamic regions.  The fixed space is used efficiently
regardless of the complexity of the problem being solved.  The dynamic
space contains the symbol table, the expression lists, and several other
data structures whose sizes vary.  The space for these structures comes out
of a common pool; they play against each other.

Every identifier in an ODE description requires 254 bytes; this space can
never be reclaimed.  Each operator in an equation costs 14 bytes.  For the
sake of speed, some operators are expanded into a sequence of simpler
operations, each of which requires 14 bytes.  In particular, some common
powers are reduced to sequences of square roots, squares, cubes, and
inversions.  The extent of these transformations can be seen with the help
of the `examine' statement.  The operator space for an equation is
reclaimed whenever a new equation is defined for that particular dynamic
variable.  Each lexical token in the most complex statement costs 10 bytes,
but this space is reclaimed during the scan.  Thus, the most complex
statement circumscribes this requirement.  Each element in a `print'
statement costs 6 bytes.  Upon execution of the next `print' statement this
space is reclaimed.

In addition to these absolute space limitations, the maximum depth of the
operand stack used for evaluating derivatives is 30 cells.  This limit
could conceivably be exceeded for a very complex differential equation.
The solution is to simplify the equation if possible, and where not, to
recompile ode with a larger operand stack.
